{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 导入必要的库\n",
    "\n",
    "我们需要导入一个叫 [captcha](https://github.com/lepture/captcha/) 的库来生成验证码。\n",
    "\n",
    "我们生成验证码的字符由数字和大写字母组成。\n",
    "\n",
    "```sh\n",
    "pip install captcha numpy matplotlib tensorflow-gpu pydot tqdm\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-06-16T06:51:38.735536Z",
     "start_time": "2019-06-16T06:51:38.469537Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\n"
     ]
    }
   ],
   "source": [
    "from captcha.image import ImageCaptcha\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import random\n",
    "\n",
    "%matplotlib inline\n",
    "%config InlineBackend.figure_format = 'retina'\n",
    "\n",
    "import string\n",
    "characters = string.digits + string.ascii_uppercase\n",
    "print(characters)\n",
    "\n",
    "width, height, n_len, n_class = 128, 64, 4, len(characters) + 1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 防止 tensorflow 占用所有显存"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-06-16T06:51:40.012307Z",
     "start_time": "2019-06-16T06:51:38.736824Z"
    }
   },
   "outputs": [],
   "source": [
    "import tensorflow as tf\n",
    "import tensorflow.keras.backend as K\n",
    "\n",
    "config = tf.ConfigProto()\n",
    "config.gpu_options.allow_growth=True\n",
    "sess = tf.Session(config=config)\n",
    "K.set_session(sess)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 定义 CTC Loss"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-06-16T06:51:40.019037Z",
     "start_time": "2019-06-16T06:51:40.014648Z"
    }
   },
   "outputs": [],
   "source": [
    "import tensorflow.keras.backend as K\n",
    "\n",
    "def ctc_lambda_func(args):\n",
    "    y_pred, labels, input_length, label_length = args\n",
    "    return K.ctc_batch_cost(labels, y_pred, input_length, label_length)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 定义网络结构"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-06-16T06:51:41.015916Z",
     "start_time": "2019-06-16T06:51:40.021386Z"
    }
   },
   "outputs": [],
   "source": [
    "from tensorflow.keras.models import *\n",
    "from tensorflow.keras.layers import *\n",
    "\n",
    "input_tensor = Input((height, width, 3))\n",
    "x = input_tensor\n",
    "for i, n_cnn in enumerate([2, 2, 2, 2, 2]):\n",
    "    for j in range(n_cnn):\n",
    "        x = Conv2D(32*2**min(i, 3), kernel_size=3, padding='same', kernel_initializer='he_uniform')(x)\n",
    "        x = BatchNormalization()(x)\n",
    "        x = Activation('relu')(x)\n",
    "    x = MaxPooling2D(2 if i < 3 else (2, 1))(x)\n",
    "\n",
    "x = Permute((2, 1, 3))(x)\n",
    "x = TimeDistributed(Flatten())(x)\n",
    "\n",
    "rnn_size = 128\n",
    "x = Bidirectional(CuDNNGRU(rnn_size, return_sequences=True))(x)\n",
    "x = Bidirectional(CuDNNGRU(rnn_size, return_sequences=True))(x)\n",
    "x = Dense(n_class, activation='softmax')(x)\n",
    "\n",
    "base_model = Model(inputs=input_tensor, outputs=x)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-06-16T06:51:41.133269Z",
     "start_time": "2019-06-16T06:51:41.017246Z"
    }
   },
   "outputs": [],
   "source": [
    "labels = Input(name='the_labels', shape=[n_len], dtype='float32')\n",
    "input_length = Input(name='input_length', shape=[1], dtype='int64')\n",
    "label_length = Input(name='label_length', shape=[1], dtype='int64')\n",
    "loss_out = Lambda(ctc_lambda_func, output_shape=(1,), name='ctc')([x, labels, input_length, label_length])\n",
    "\n",
    "model = Model(inputs=[input_tensor, labels, input_length, label_length], outputs=loss_out)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 网络结构可视化"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-06-16T06:51:41.687532Z",
     "start_time": "2019-06-16T06:51:41.135134Z"
    },
    "scrolled": false
   },
   "outputs": [],
   "source": [
    "from tensorflow.keras.utils import plot_model\n",
    "from IPython.display import Image\n",
    "\n",
    "plot_model(model, to_file='ctc.png', show_shapes=True)\n",
    "Image('ctc.png')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-06-16T06:51:41.696732Z",
     "start_time": "2019-06-16T06:51:41.688981Z"
    }
   },
   "outputs": [],
   "source": [
    "base_model.summary()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 定义数据生成器"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-06-16T06:51:41.706826Z",
     "start_time": "2019-06-16T06:51:41.698386Z"
    }
   },
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "d:\\anaconda3\\envs\\py36\\lib\\site-packages\\tensorflow\\python\\framework\\dtypes.py:523: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.\n",
      "  _np_qint8 = np.dtype([(\"qint8\", np.int8, 1)])\n",
      "d:\\anaconda3\\envs\\py36\\lib\\site-packages\\tensorflow\\python\\framework\\dtypes.py:524: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.\n",
      "  _np_quint8 = np.dtype([(\"quint8\", np.uint8, 1)])\n",
      "d:\\anaconda3\\envs\\py36\\lib\\site-packages\\tensorflow\\python\\framework\\dtypes.py:525: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.\n",
      "  _np_qint16 = np.dtype([(\"qint16\", np.int16, 1)])\n",
      "d:\\anaconda3\\envs\\py36\\lib\\site-packages\\tensorflow\\python\\framework\\dtypes.py:526: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.\n",
      "  _np_quint16 = np.dtype([(\"quint16\", np.uint16, 1)])\n",
      "d:\\anaconda3\\envs\\py36\\lib\\site-packages\\tensorflow\\python\\framework\\dtypes.py:527: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.\n",
      "  _np_qint32 = np.dtype([(\"qint32\", np.int32, 1)])\n",
      "d:\\anaconda3\\envs\\py36\\lib\\site-packages\\tensorflow\\python\\framework\\dtypes.py:532: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.\n",
      "  np_resource = np.dtype([(\"resource\", np.ubyte, 1)])\n"
     ]
    }
   ],
   "source": [
    "from tensorflow.keras.utils import Sequence\n",
    "\n",
    "class CaptchaSequence(Sequence):\n",
    "    def __init__(self, characters, batch_size, steps, n_len=4, width=128, height=64, \n",
    "                 input_length=16, label_length=4):\n",
    "        self.characters = characters\n",
    "        self.batch_size = batch_size\n",
    "        self.steps = steps\n",
    "        self.n_len = n_len\n",
    "        self.width = width\n",
    "        self.height = height\n",
    "        self.input_length = input_length\n",
    "        self.label_length = label_length\n",
    "        self.n_class = len(characters)\n",
    "        self.generator = ImageCaptcha(width=width, height=height)\n",
    "    \n",
    "    def __len__(self):\n",
    "        return self.steps\n",
    "\n",
    "    def __getitem__(self, idx):\n",
    "        X = np.zeros((self.batch_size, self.height, self.width, 3), dtype=np.float32)\n",
    "        y = np.zeros((self.batch_size, self.n_len), dtype=np.uint8)\n",
    "        input_length = np.ones(self.batch_size)*self.input_length\n",
    "        label_length = np.ones(self.batch_size)*self.label_length\n",
    "        for i in range(self.batch_size):\n",
    "            random_str = ''.join([random.choice(self.characters) for j in range(self.n_len)])\n",
    "            X[i] = np.array(self.generator.generate_image(random_str)) / 255.0\n",
    "            y[i] = [self.characters.find(x) for x in random_str]\n",
    "        return [X, y, input_length, label_length], np.ones(self.batch_size)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "# 测试生成器"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-06-16T06:51:41.909160Z",
     "start_time": "2019-06-16T06:51:41.708359Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[[[0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  ...\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]]\n",
      "\n",
      " [[0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  ...\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]]\n",
      "\n",
      " [[0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  ...\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]]\n",
      "\n",
      " ...\n",
      "\n",
      " [[0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  ...\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]]\n",
      "\n",
      " [[0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  ...\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]]\n",
      "\n",
      " [[0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  ...\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]\n",
      "  [0.96862745 0.9882353  0.9372549 ]]]\n"
     ]
    },
    {
     "ename": "NameError",
     "evalue": "name 'input_length' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[1;31mNameError\u001b[0m                                 Traceback (most recent call last)",
      "\u001b[1;32m<ipython-input-8-62541555690a>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[0;32m      4\u001b[0m \u001b[0mplt\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mimshow\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mX_test\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m      5\u001b[0m \u001b[0mplt\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mtitle\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m''\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mjoin\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mcharacters\u001b[0m\u001b[1;33m[\u001b[0m\u001b[0mx\u001b[0m\u001b[1;33m]\u001b[0m \u001b[1;32mfor\u001b[0m \u001b[0mx\u001b[0m \u001b[1;32min\u001b[0m \u001b[0my_test\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;36m0\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 6\u001b[1;33m \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0minput_length\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mlabel_length\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[1;31mNameError\u001b[0m: name 'input_length' is not defined"
     ]
    },
    {
     "data": {
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAuEAAAGrCAYAAABua0kFAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAWJQAAFiUBSVIk8AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjAsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8GearUAAAgAElEQVR4nOzde5Rl51nf+d9zzqlzqWtXt7pblt2SJVmXtmQLy/JNimRhAssx2CQgJ5oMBDKJZ8WwQmyHrJkJJjEEVshkJg4mxAwmAwPMjEzkwQyJ4yHgi2QLY0uyrFtLslotd8u6tPpaXZdzf+ePcwpV1/Ps7lNdp3bdvp+1tEr11L7VqX12vbV7/97HUkoCAAAAkJ/Ceh8AAAAAsN0wCAcAAAByxiAcAAAAyBmDcAAAACBnDMIBAACAnDEIBwAAAHLGIBwAAADIGYNwAAAAIGcMwgEAAICcMQgHAAAAcsYgHAAAAMgZg3AAAAAgZwzCAQAAgJwxCAcAAAByxiAcADYQMxs1sw+a2R+b2WEzmzezOTM7ZGZ3m9mPmVmtv+z7zSyZWcfMdpxjmwf6y3XNbNc5lnukv9y/O8cyN5nZvzWzh8zsmJm1zOy4mX3NzP5nM7s+WOcn+9td/t+CmT1rZn9gZt+/0tcKADYzSymt9zEAACSZ2Xsl/aaki5eU5yR1JU0sqT0v6cclPSbpxX7tfSmlPw62uVvS0SWlv5FS+myw3E5JxySZpL+ZUvqPy74+Lum3JP2tJeWOpNOSpiQVl9TvSin9N0vW/UlJv93/9KUly+2QVFny+cdTSh9ZfmwAsBVxJxwANoD+QPWz6g3An1RvkH1RSmk8pTSp3oD1DklfknSJpNtSSi/1l5Wk2zI2fWv/40sDLGf9/79n2bFNSPqKegPwtqRPSnqLpHJKaZeksqQ3SvpF9Qbld2Z9nymlixf/kzTaX++L/S9/2Mzek7UuAGwlDMIBYJ2Z2Rsl/YZ61+TPSXpTSun3U0rHF5dJKZ1OKX0mpfS96g2Gz/S/tDhgzhpcL9Y/PuByT/YH90v9pqQbJNUl/WBK6adSSvenlLr9Y+umlB5JKf1zSVdK+i/n+ZYXv6duSukRSX9D0sv98t8ZZF0A2OwYhAPA+vtl9R7L+K6kv51SWjjXwimlP5D0b/qffrn/8UYzGwsWX7wT/geSDkr6nv6d7azlvry0aGY36pU72x9NKf3JeY7tuKQfOtcywTqnJX29/+nrV7IuAGxWDMIBYB2Z2asl/WD/00/0B6TnlV4J9CwOmkuSbl627Qn17mB/N6V0SL1HSorBcuOS3rRse4v+Qf/jCUm/PuCxdQdZbpnFR2GK51wKALYIBuEAsL5u1ysD0P93pSunlJ6TdKj/6fJHTW5Rb1B7b//zezOWu1m9Qby07HlwSd/b//hfU0r1lR7fIMxsStJb+58+sxb7AICNhkE4AKyv/f2PDb0SslyprOfCFz9fHHx/5TzLPdMf1EuSzGxE0uv6n37rAo8tk/W8QdL/I+mifvn3h70fANiISudfBACwhhbn7T655BGTlbpH0k9IequZVVJKjX791iVfV0rpSTM7KuktGcstfxRl55L/P3GBx3YWM3txyafLpyj8bUn/UQCwDXAnHAA2v8XBc1X9xzrMrKreNIIn1ZtPfNFX1Rv4vq2/XEWvPAqy/FGUtbB3yX+LA/Ak6b9PKf13F/g8OQBsOgzCAWB9LU5DOG1mds4lM6SUDqo3s4r0yqMlb1VvkPuVZXfYlz8X/lb1Bu+SvxO+9O73Tg1BSslSSqbev8ReLulfqDcI/9dm9uZh7AMANgMG4QCwvg70P1YkXbOK7Sx/Lnz58+CLlj8XvvjxSH8Glb+UUmpJerr/6Q2rODYnpdRJKT2bUvpnkn5eva6bf5AxzSIAbDkMwgFgfX1ZvTvBkvS+VWxncRB+s5kV9cpz3ssH4d+UNCfpHWZW0rLnxgOL3Sy/v/+Iy1r41+rNinKFpJ9do30AwIbCIBwA1lF/NpLP9T/9h2Y2Och6waMri4+SjKv3LPg7JM1LemDZ/tqSvtZf7ia9Mmf48kdRFv1m/+NOST91gcd2Tv077r/S//Qfm9n0StYHgM2IQTgArL+PqjdF4Wsk/V/nu+NsZn9T0keW1lJKB/RK6/d/JGlC0tf6A9zlvrJsOSljEJ5Sul+9bpuS9Mtm9v3nObZdkv7zuZbJ8LuSXuofzz+6gPUBYFNhEA4A6yyl9JCkn1bvsZQflPRNM/sxM/vLMKSZTZnZj5jZFyV9Wq8MnpdafKTkb/Y/Ln8URcvqi8u9lFJ66hyH+AFJj6gX4Pycmf26md1oZoX+sZmZXW9mvyDpoKS/dq7vN9KfLvHX+p/+zKD/IgAAmxWDcADYAFJK/0HSj0g6KulaSb8n6biZnTGzGUmnJH1GvQ6b35H0hWAzi4PwxWt71iD8a5LaS5bLehRl8dhmJP0VSXerN6vJT6n3mEvTzI5Laqo3SP9nkibVu6t9If69pFlJ0+r9UQIAWxaDcADYIFJKn1UvnPjT6j0n/px6g96SpGfVGwT/bUnXpJSiIOXSwfTis9/RfubUC2guOu/84CmlmZTS+9Wb0vDX1Bt0z6g36J7p7+tXJO1PKf3E+baXsY+Tkn6r/+mHzWz0QrYDAJuBXXiDNgAAAAAXgjvhAAAAQM4YhAMAAAA5YxAOAAAA5IxBOAAAAJAzBuEAAABAzhiEAwAAADljEA4AAADkjEE4AAAAkLN1HYSb2WvM7H83s+fNrGFmz5rZvzWz6fU8LgAAAGAtrVvHTDO7UtJ9kvZI+iNJT6jXDvl7JT0p6ZaU0vF1OTgAAABgDZXWcd//Xr0B+M+klH5tsWhm/0bShyX9sqR/cCEbNrNDkiYlPbv6wwQAAAAyvVbSTErp8pWstC53ws3sCkkH1RskX5lS6i752oSkFySZpD0ppbkL2P7xWq2289r9Vw/piAEAyNepU0fD+o4de3I+EgDn8sSBp7SwsHAipbRrJeut153wd/U//snSAbgkpZTOmNlXJf2ApLdL+rOsjZjZAxlfql67/2rd9/UvDeNYAQDI3R//p4+H9ff+0IdzPhIA53LzW2/XNx/81rMrXW+9gpnX9D8+lfH1b/c/cisbAAAAW8563Qmf6n88nfH1xfqOc20kpfTmqN6/Q37jhR0aAAAAsLY26jzh1v+4PlO3AAAAAGtove6EL97pnsr4+uSy5YCBNBrzF7xupTI6xCPZuJrNhVWtXy7XhnQkwNYXvd9mZ0+62oEn7nW1nTsvCbf5rW/9iavt33+rq/FeBTa29boT/mT/Y9Yz31f1P2Y9Mw4AAABsWus1CP9i/+MPmNlZx9CfovAWSQuSvpb3gQEAAABrbV0G4Smlg5L+RL3JzX962Zd/QdKYpN+9kDnCAQAAgI1uPTtm/pR6bes/YWbfJ+mApLep17b+KUk/t47HBgAAAKyZdRuEp5QOmtlNkn5R0rslvUe9TpmfkPQLKaUT63Vs2LzqdR/MPHjwW662Z88+V8sKMUX1SsXXarWxQQ4xV4OGwo4cedTV9u27Ptzm+LivEQADBhe931Yi670JYHNZzzvhSikdkfR31/MYAAAAgLxt1HnCAQAAgC2LQTgAAACQMwbhAAAAQM4YhAMAAAA5W9dgJjaPaNaRhQU/jXu0XLebMrZqrlIoFF2tVvPt5GvVuMV8tP8TJ152taMvPeNqY+PT4TaLRT8dyJve5FtEr6esVvSDtseOZM3gELXHxsaXdY4Mgtlvhiua3WRlMxT5axU/I2Dz4U44AAAAkDMG4QAAAEDOGIQDAAAAOWMQDgAAAOSMYOYWVK/7AFZ9wQcWGw1fkyTz2Ui1mnVXe/KpB1ztzOlZV0sZucwUBjNHXG33Ra92tevfcGO4zSgE2gjCmnNzJ1zN/OFIkq67/o3xFzaB1bTHzgqFtVotV6vXG67WDM6ZbrcdbrNg/n5AuVz1tYoP5FarfrntbCUh3UHDgOM+m0wQMDBo+DV67VYSeOa1B7YG7oQDAAAAOWMQDgAAAOSMQTgAAACQMwbhAAAAQM4IZm5yUdfKU6eOu9pDD33N1VqtM+E2a6M+HNnpNF0tCnY2GkEwKTOY6XWDsOZ83Z+mWQGoFKRAW+1ufADLjI5NhPVabWyg2ka0ms58UVc+SWoHr+czB59yte8+52u10azuqVGn1L2udt31PpC7nYOZ0fsgCmBKq+uUSpfUs61F+DUMIhPABLY07oQDAAAAOWMQDgAAAOSMQTgAAACQMwbhAAAAQM4IZm5AcUfCOAh0+vQxV/vG178QrO8DQ1bshNtc8I0OVSr54Fyh4P+Gq1Z9qNOCsKUUhyibbX9MZn65borDllF3zmg/0XLtVtzNsdPxx9Tp+GWLxfV7O2UFuKJOh4OG7LK2OT/nz6VTp3zt5ZdfcLXaaPwaV4JwZbHoDz7qwilNhdvcrlbTJVXK7pS6XRF+BbBWuBMOAAAA5IxBOAAAAJAzBuEAAABAzhiEAwAAADljEA4AAADkjNlRNqCoFf2hZ3wLcEk6eNCn7IsF306+XPbrFgrxjz9qLN7p+NlIUjeYXaXgZ1EpFePZUWS+PlKquFqz4ZcrFvwsLJLUkp99I2t2luXa7XjmjpMnTrja1JRvqb6es6NkWYu2152uPxc6HT+jjwr+TCqV/fkhSZWK/7lXa34/5ehExlmyZjcZtH36+Pi0q9E+/WybeQaaaLaXlYjOhazZu6KZviLVqt/myEh8jQe2Eu6EAwAAADljEA4AAADkjEE4AAAAkDMG4QAAAEDONl6SDKrXfTDz8OHHwmU7HR8aLJj/26o84n/UUQBTkpptH6aZmfHH1O34LUxNjwVbjMN4qevX7wTb3LvXhyC7wbo9UQgzqAWrN5vNcItRvdXytShcuBV1g0Buu91wtYkJH6IsFOKgVik4Py/a/VpXI6x1tigkNz4eLztoW3RCmOe3EcOvgwYus5Z78cWnXW16+tWu1g2C2fNz8TaffPJxV5uc3OVql152matNTOxwtSjAKXFd2EjWIvi7lXEnHAAAAMgZg3AAAAAgZwzCAQAAgJwxCAcAAAByRjBzA+p2g86NFndztKBDZLHoA3EpBSHIjHBjs+mDN41G1L3QByhKxVFXGwlCd5J06tRpV1uY96GOSuWIq1166RXhNltBiLLZ9KHB6DvP6u52ZvaMq2V119wOuh0fzLTgFbXkz5lCIe5eWgjO2ZGR6kA1nG27BZvW2mYJvzabdVc78h0fFB2b8KHQ3jH5a/fjj9/j99Pw174zZ/w1UpLqdX9dOPryIVd74YWDrrb34ktd7aqr9of7GR+fcrVazX8/GK4ohDk7e9LVsjrMxqFlv9xWvqZxJxwAAADI2VAG4WZ2h5n9mpnda2YzZpbM7PfPs87NZvY5MzthZvNm9rCZfcjM4vnsAAAAgC1iWI+jfFTSDZJmJT0n6dpzLWxmPyzpM5Lqkj4t6YSk90r6uKRbJL1/SMcFAAAAbDjDehzlw5KuljQp6YPnWtDMJiV9SlJH0u0ppb+XUvonkr5H0p9LusPM7hzScQEAAAAbzlDuhKeUvrj4/2Zx8GqJOyTtlvS7KaX7l2yjbmYflfRn6g3k7xrGsW10Cwu+E2UUdrCCD7lJUqHon96Juk62wzCdD8NJUrHgu16+6lW+c9pI2Xcpe/3+N7ra3Fwc2ukcfCSoBp05gw5tC/WZcJtRZ9Fu1wczi0V/nrbbccfM+bnZYNntG8xMyZ9L0TkbBoxb8XkcdV+tlH0H0kKBGMuFatR9cG9QlSqB2KU2YlBs0JDciy/6EGSWemPe1eaDjs7KCPkXi0FgOxgjnD79nN933R/7Sy9+J9zP297+fcF+drtaVsdNnN+g59eBJ+4deJtRYHPQcPNWsR6/0d7V//j54Gv3SJqXdLOZbY8e4AAAANh21mOKwmv6H59a/oWUUtvMDkm6TtIVkg6ca0Nm9kDGl875TDoAAACwntbjTvjihJ5+kuiz6ztyOBYAAAAgdxuxWc/iA2PxQ2ZLpJTeHG6gd4f8xmEeFAAAADAs63EnfPFOt29x1TO5bDkAAABgS1mPO+FPSrpJvSkNz3qm28xKki6X1Jb0TP6HtrYWFnzS/PTp4672zW9+xdWKxXjmjihpXm/42UCazWD2iUrc1veGG252takdO12tWo1a1PsZU7ImzLlo16tc7ZmDT7razov8k0mHnn0w3GaU4K7W/Pfe7fi/P1uteMaTTsfXO8FsM9tFN5gdpduNZkcJllM8O0pjwb/GKfkTh9lRzi9rFpSZM34mgyef8LMJXX21j9RMJN/qvFbzsyhhYxl0RouVGAlm5OpmzN41Etzn6yZ/PR4p+XkY5ub8LCyn661wP1//2pdd7R03BzOmTPtrSqXCzD8XKqsd/aCitvXbzXr8RvtC/+O7g6/dJmlU0n0pJT+SBAAAALaA9RiE3y3pmKQ7zeymxaKZVSX9Uv/TT67DcQEAAAC5GMrjKGb21yX99f6nF/c/vsPMfqf//8dSSj8rSSmlGTP7gHqD8S+Z2V3qta1/n3rTF96tXit7AAAAYEsa1jPh3yPpJ5bVruj/J0nfkfSzi19IKX3WzN4p6eck/aikqqSnJX1E0idSCh4aAwAAALaIYbWt/5ikj61wna9Kes8w9r9ZNIIWwA998z5X63R8m/RORkv1btAuuNHw4ZVC0bfrnZx8TbjNvRf7+vS0D2au1siID+NMTPpJcw4c8EHVWi0OlXbaPkpQCwKk9aD1chRe7dV90C2lOIi0HRSCoK35rJbM/OvZzsiztoNMbHf7vsSrEoXxJOnAYw+52pm5513twQdfcrW3ve37XY1g5taSwtC0f7On4HdOpxO/WVNwDTDzT8EWi742Pu4Dk41GvJ8zMydc7ZGHfS+/m956i6sRzLxwUbAyCmtmBTDHx33gu1z2Y5VI1nVuUIPuZ60x1QAAAACQMwbhAAAAQM4YhAMAAAA5YxAOAAAA5Gw9OmZuW1GQoFAMehKlqHNj1t9Lvl4IOppNTux2tTfe8KZwi1mhx2Erl8tB1Sf3rrzSH2dWp649u69wtSOHfVfAhQUfzOxmhC2jegq6QW4XhSBEVav6kG2zEZyzQahLkgoFfy5EAa7tLAp2R9eU48dfCNdvtY+62uwZ37G3WPTv/6jb5viE72QrSSMj0fsaG0VWqLwT1FstXysGyexORop6dt5PElCt+PNjZMQPRQrBfsqV+JqwUD/jajNnXna1etC1WkE3aHhRkHF83C+3f/+tq9pmZCWdXwcNhkbHnnU80f6HFezktxwAAACQMwbhAAAAQM4YhAMAAAA5YxAOAAAA5IxgZo4KBf83TxRSmZ/zgZRWMw4Cdrp+/e+54W2uNjnlg5lTU3EgpVpdvw5iqw1/vPzyc66WgtM86jQadXjsLeu7lSbFy24HUXe7csWHgaPzvVCIX7eRsj/nClEbzm2s2fDhoCee+JqrnToVBzOjrrtmPlBXMH/9efmo3+Yll1we7gcbW6cTvwc7yb/fut2oPW7QRTPjft5Fu/3vmPq8X3Z27pSrlcs+1DkyEl8TqtXg+hMcepc2vEO1Fl0nBw1hHnji3oG3GYU1ozFEVhfOaP9fve83zvr81CkffB8Ed8IBAACAnDEIBwAAAHLGIBwAAADIGYNwAAAAIGcMwgEAAICcMTvKKtXrvg1u1BJdkk6dOhas79O4KZy5YyTc5sSkT5+/6pLLXG16ek+4/mawkgR2qRS1T/cp+yglP1r17bolqTrq91+rDT8VvlkEkyPIgmLUdrpYjGc3KAT1qLadNcJZA0642sxpf52RpE4wMUT0fikW/bl96aVXDXCEWE/R741uCmrBcpJUMP9zv+66N7raiy8edrViKb6fd9XrbnS1hQU/S88D9/uZLmbnfNv50VrG7EolP5SJZlIpFoOLFza8aHaTlYja1jeb9aAWz46ykplYVoo74QAAAEDOGIQDAAAAOWMQDgAAAOSMQTgAAACQM4KZq1Rf8A/yP/jAn4fLHj12yNXGa0Gr4Ch7khHMvO66t7ja2NiEq0WtxreiKHQU1aIQU602Fm4zeo2rtTjEuT0EIcwB/54vlcphfWzUn7OlIGy1XTQaPvAdhYZaLR86Llf8aylJrbavVatTrnbjjbe52sTkDlcbGYl/llgfUQiz0fAhyCyjY/79Vq36a+Ib3nCLq9UyrofVIOw+MzPrarsvutTVFg6fcbV2O/5+Rkr+mjRS9id8q+XfQ9HkClJ87FgfUbAyK6wZLTs+Pj3Qfg4e/MbKDmwItsfIDAAAANhAGIQDAAAAOWMQDgAAAOSMQTgAAACQs+2bfBqSTtB5cX7eB08kqT7fcLWR6M8g82HN2qjvjClJlYoPzkQBw6xOUINaSdfK9ZSCcFKr3RlouWq1Gm6zXPH1yiZ5PdaTBQHOUikOGE9O+IBg1rLbQdTN7elvP+Rq9QW/XFIczLz++je52u7dF7taFEjLCi1j4+gELVE7nbjDZKTb8aHHI4efcLU33/RXXW1q6qKB91MLul5O79ztavPzl7havT4TbrNc9scehTCfevpBVxufiEN7BDPXRzTWGB/3y+3ff+uqthmNiaJQpxSHQG+5+W+d9fm//MU/kvTCwMe0iDvhAAAAQM4YhAMAAAA5YxAOAAAA5IxBOAAAAJAzgpmr5sNnl1xyWbjk3Jx/aN8KPkwTdszM+nvJ/P6jwMHs7ElXW1nHKb/cRgxrBnlLKahFi7U6PsApxV1Ru0Egt1DYWn/TNptxd7pm0weMo2Wj13ikHHdZjOrljGU3q0bDv271hbhb3/Hjz7vamTOnXO3oy752ySU+bClJ0zv2+tq0D8RhcwqDhFZxpXIlvm43Gv46Vxvz18QoNLwS5bIPXF919dWu9qpX+fP48OEnw22eOum7Uc/P+/dGoeTfb9H3jY1lLcYagwZApZWFQFdqa40aAAAAgE2AQTgAAACQMwbhAAAAQM4YhAMAAAA5I5i5AgsLc0HNd8c8fOTb4fpF3wgzylWqVPJhmqxAW7HgNxCFMA88cW+4fiQKbK5lMGGYoi6NZtFp7pfrZgQzW+22q7WDWqm0urdTFKiNalHALyu4Ui4H3T6DDqCRRj0OLB1+1oegGo0grBWkZIPTXZI0Pb3D1YrFrXV5qtd9KOzAAd8FU5Kee853Khyt+ddjesqHLacm47BlJaMjLLaGSsUHM1+//82u9q1vfSlc38yHq0eCa1pxlQH0KHAd1UZH/fdTLPlAvCQdePxFV5tf8MHMUnBNseiXMLal9ZhsgjvhAAAAQM5WPQg3s11m9vfN7A/N7GkzWzCz02b2FTP7e2YW7sPMbjazz5nZCTObN7OHzexDZhbcLwYAAAC2jmH8e+/7JX1S0guSvijpsKS9kn5E0m9J+mtm9v6UXvm3aTP7YUmfkVSX9GlJJyS9V9LHJd3S3yYAAACwJQ1jEP6UpPdJ+s8ppb98YMvM/qmkr0v6UfUG5J/p1yclfUpSR9LtKaX7+/Wfl/QFSXeY2Z0ppbuGcGwAAADAhrPqx1FSSl9IKf3x0gF4v/6ipN/of3r7ki/dIWm3pLsWB+D95euSPtr/9IOrPS4AAABgo1rr6Qda/Y9Lp5J4V//j54Pl75E0L+lmM6uklPw0EOsomh3lwQe/7GonTnw3XD+anKBa8C18S0Ht2mt8K/neNv1GDx26P1hycFHb+k0jiCBY8LdmFFVotVuuJkkzM6ddbc8ePzvKoBYyWpXPzp5wtW8f/IartZp+39VyPCPGlVde62o2NXW+Q5Qk1TNmRzl92s86sLDgl03yMxlYMJuPJBWCGRdKpa0VD4lmRzn6sp9pRpLa7TOuVijsdLU3vfntrrZr16vDbVYqftYlbB21mp9NZHJql6tNTPh28JI0N+uvKymY4WjZ/bZerRvPWmKrmEmlVAp+N2bMQGUDTj0WXWeYHQXrac0G4dabF+7v9D9dOuC+pv/xqeXrpJTaZnZI0nWSrpB04Dz7eCDjS37kAQAAAGwQazlF4a9Iul7S51JK/9+S+uJtOH978ey6nzgYAAAA2ALW5E64mf2MpH8s6QlJP77S1fsf/b+DLZNS8p0I9Jd3yG9c4X4BAACAXAz9TriZ/bSkX5X0uKTvTSktf9B18U531oOpk8uWAwAAALaUod4JN7MPqTfX96OSvi+ldDRY7ElJN0m6WtJZz3T3nyO/XL0g5zPDPLZhiIJV8/O+bf1C3Qc4Jala9cGZeDnfOjWqZdWjYGXUij4rgDk+Pu1q69HO9UIUiz5kE4V5Ci2/XLMVBzPPnPEhuVbGsoOoBwFfSfrmA/f6ZVsvu1qz2XG1atm3nJakyalxV/vu8w+7WnQuLGQEM1tNv69u1x9TCv4xa2Ehft263fP+w9emZ+bDa5VqfAlemPevR6nkg5UTE5OuNrWDJ/nQE7Wyv/rq68JlH3/MB8OjwGW94edL6AYBTkkadrQ6YzdxPSja1r/MYJMZ2p1wM/sf1BuAP6TeHfBoAC715gKXpHcHX7tN0qik+zbazCgAAADAsAxlEN5vtPMr6t3Z/r6U0rFzLH63pGOS7jSzm5Zsoyrpl/qffnIYxwUAAABsRKt+HMXMfkLSL6rXAfNeST8TzLv5bErpdyQppTRjZh9QbzD+JTO7S7229e9Tb/rCu9VrZQ8AAABsScN4Jvzy/seipA9lLPNlSb+z+ElK6bNm9k5JP6deW/uqpKclfUTSJ1LUIQAAAADYIlY9CE8pfUzSxy5gva9Kes9q95+rFHTWSv4ltKAmScVisGzwQFAho6tgJApMjvssnvbvv3VV29wsisFrNz7h40GNpl+u04lDg+2gk2anvZqOmXEws1Gf8bWGDwMXS2VXm52LIxjPPOO3uWPad16Mgrs7pq4Itzlox7wk/xp3O/ETcN3uhXetazbjAOmg1uJ873SCoGrwupUy3uvFgj9nm61om3T7Q7aoo3JpxF8/JGkkqM/O+uvHyeM+LF4pj4XbHBv3weGos+egorC3JHWD91unEwXD665WX4jjZ40ggErXWQzbWjbrAQAAABBgEA4AAADkjEE4AAAAkDMG4QAAAEDOhtoxc1sKciIWBNIkqRDVg/WjANdKbOZg5WoVgo6ZUVgzCs87BpkAACAASURBVL+22/HrHnVK7SYfBBpUoRD/7VsIwngFi2r+pKlW48CSDdgiLuqYOTMTB0jbHR9KjfZiwfdTLPqgmCRZ8H1GohDm7OxJV4uCplL8fUZB5tW+h6Lw7bGXfXi20YhDpYWCvzSXyz7kpgFfN2BRpxNf504H7/d2y5+fTxy439WOvvR8uM03v+U2V1tNMDMjl6loQrXoGn16xn8/Tz8dN+feMb3H1QhmYti4Ew4AAADkjEE4AAAAkDMG4QAAAEDOGIQDAAAAOWMQDgAAAOSM2VFWwIIZNUrBKxjVJKlY9H/zdIN2392g3W63O9gsF9tekJKPWx0Hr3HGrAHdrk/ZR22SI42Gb5PcaDTDZTvdKHnvz5noPMqaJaM26mfUeN2Vb3e18fFpV6sv+FlQJMWvcTCjT9FGXC1r1pFoxphBZ0I58MS94TYj0awp+/ffOvD6g1pY8DPqHPrOk67WDGbekaRqMIPE2FjQArwatwsHsmRNvhX8Kgpbt5dK/vrVSbPhNptNf/1bjej3pRTP+BL+zjR/TSuW4tnMsP1Ev3OyDGsWOu6EAwAAADljEA4AAADkjEE4AAAAkDMG4QAAAEDOCGauQNRtfGyi7GrdFD+wXwxaqif59TvtIIwXtLGG1wnCOK2WD+OEoZ0gcChJ7bYPF3Wz0k3LRMHMgwd9QE+SZs6cdrWyPz3UCcJJlvH3dLW819VKJd86Pg6ZZH2PPpRqwbLFoj9ni8U4QBotG8lqRz+oqG39Wmi1fKCtE9Si5SRpfGLK1S577RWuVqnSRhsrYxkh7mLRh3yTTgTL+WtNrRZcqCQVgt95q5FxiVaj7q/x0SV6pOyvMxdf7K+R59wZtoRBg/9S/Hvnhht+YCjHwZ1wAAAAIGcMwgEAAICcMQgHAAAAcsYgHAAAAMgZab8VMPMhk0rFv4T1kfhvmyjQkpLvKjg9/SpXKxT9cvCijmoL9dZAy8myOmb69aMOkZF60Dlxfv5MuOzs7IyrlSt+3ztKUYgyPudOnfLBqqi7XMQyXo9i0QdaomUrFR8AnZzcEW4zWjYSBSuj0ExWADPqDDqszmdLRdeKKJgdLdc7Jh90K1eCWplgJlamVou7rFaC98HpzmBdnk3xeZx1fg/bSNmHTaNY5cKCv55+59Az4Tb37t232sPCBrEW3Zf/+D99/KzPT506uvIDE3fCAQAAgNwxCAcAAAByxiAcAAAAyBmDcAAAACBnBDNXIAUdtBrNoBtjRqOtqBwFq8oVX6uUBwuubXdRJ8xuJ6gFy9Vqo+E2y2Vfr9UGC/NFP/NWM+6S2O02g5rfQrvtg1HdbrzNkZFZV+t0fDgpUirFXfBGRnxIuN3xtaib46WXXhpusxKc81E4cXzcr7t//63hNiNrEcKMROG3sdGdrtZNc/EGgjwbDfwwDJVq/Lvkqqtf72qnTn/X1VLyYfO8ROHmzHoQCg1/P/C+2pZW2335vT/04bM+/5e/+EeSXljxdrgTDgAAAOSMQTgAAACQMwbhAAAAQM4YhAMAAAA5YxAOAAAA5IzZUVagE8yy0ax3XC2ajUOS5Dvrygq+OD29y9VGyrStH0Q0g0Q7aNMepeQr1Xh2lNe//iZXy2r97A/IJ/SzZmEZH/czdyzU/Ywp8wu+Vq3E50ch+DM7qkVKpbgl+s5dr3O1l156PFjSzxyU5N8vPVn1s+U1u8lqFQv+0nrFlde42iOPvBiu324FrwezOJxX1J56JTbL+bUa1YzZUarBNW1ifLerzc09N/RjWljwswQ1m/46d+bM6XD9QsG/OUrBha4UfI9XXb0/3Ga1woxkW9m+fde7WtaMKdGyw8KdcAAAACBnDMIBAACAnDEIBwAAAHLGIBwAAADIGcHMlQiCUSkI3kW1LFG77onJSVcbGYlbiGOZKLw24M+oWIhf41rVh3kGDWYWij54G7V9l6SxsQlX63R9i+hu1wdNLQgmSdLYhA8XFYqD/e1dLMavR2kk2qZfttnyIbmDB+8Pt1ku+9bzUYv6zRKcKwSB62Mvv+xqxWA5SWHL7aiV/XYWhTBnZ0+62krCVpv5nFutQa9z3a6vpSgRn1GPalEw86Fv3udqzWYj3I9Ud5WC+etcpeJD8dVq/POtZARYsflE7+Hovb5/v/89tNaGcifczP6Vmf2ZmR0xswUzO2Fm3zSzf25mfqqP3jo3m9nn+svOm9nDZvYhM8v4rQQAAABsDcN6HOXDksYk/VdJvyrp/1RvfrKPSXrYzPYtXdjMfljSPZJuk/SHkn5dUlnSxyXdNaRjAgAAADakYT2OMplScv8eZGa/LOmfSvqfJP1UvzYp6VPqTQx8e0rp/n795yV9QdIdZnZnSonBOAAAALakodwJjwbgfX/Q/3jVktodknZLumtxAL5kGx/tf/rBYRwXAAAAsBGtdTDzvf2PDy+pvav/8fPB8vdImpd0s5lVUkpZKYx1EXW3LBZ9sLKwgsfai0FwrxB0+opqCASBtkLBByEtWi4jsNjp+O6FUS36WZoFndxK8flRKftw4yWv2utqJ08e9/suzIbbLJX8W7w44LmUFbaKOtlFidgoRBWFNaU4PLceIZm8WSFOW1aCDrnRObtdDBrCPPDEvQNvc7uec1miIOJV1/hukgceP+ZqjYxOpSdPHvXF4Lpy8pTf5okTz7taq3Um3E+t5n8Pl4PAZaXia7WMTsnY2jZK4Hqog3Az+1lJ45KmJN0k6a+oNwD/lSWLLfZufmr5+imltpkdknSdpCskHTjP/h7I+NK1KztyAAAAID/DvhP+s5KW3rr7vKSfTCktnZtrqv/xdMY2Fus7hnxsAAAAwIYw1EF4SuliSTKzvZJuVu8O+DfN7IdSSg8OuJnFf3ON/y387P29OdxA7w75jQPuDwAAAMjVmjxonFJ6KaX0h5J+QNIuSb+75MuLd7qn3Io9k8uWAwAAALaUNQ1mppS+Y2aPS/oeM7sopXRM0pPqPS9+taSznuk2s5Kky9WbY/yZtTy2CxF14BoJugfWM+Kk3ahTWN0H6hoN3yUxqklxB7DtLArFhrWiP/U77Xa4zdOn/d+DU1M+MBkFERsN3wmu1ToR7sfMTzIU5PNUHvHnUbEUd7cMcqED63R9+FTKCKoGXTyj4EtWGCbqXriZjQQh29fsu9TVTs88G66fgn8I3L6xzFhWJ8xBbbVzbrWqQTCzHJzHpaA2OxNf075zyP8D+PPB76xG3Qc7a9XgOpfRYbbbbbnaSMlfa/bvf6OrVWsbI6CH7SmPKTcu6X9c/M39hf7HdwfL3iZpVNJ9G21mFAAAAGBYVj0IN7NrzezioF7oN+vZo96genE+qbslHZN0p5ndtGT5qqRf6n/6ydUeFwAAALBRDeNxlHdL+tdmdo+kg5KOqzdDyjvVm2bwRUkfWFw4pTRjZh9QbzD+JTO7S9IJSe9Tb/rCuyV9egjHBQAAAGxIwxiE/6mk35R0i6Qb1JtacE69ecB/T9InUkpnPTCWUvqsmb1T0s9J+lFJVUlPS/pIf/lVPMkKAAAAbGyrHoSnlB6V9NMXsN5XJb1ntfvPkwXBzEjyGbVMzZYP4z3z7LdcbWJiZ7g+wcyzFYo+vlap+G5qzZZfrpURzDxzxndpa7V8EKjd9jGGxx/3/aQajbi7Zac942rHXj7kalPTPlzUaMYhym7X/z076N+4UQBTkup1HxJuNvzrUatNuNprXxvOKqrx8WlX2ygdzS5EFGiLwpojI3GgdnbOnwvzC/68GV3wwd9abWyQQ9z0omBlFNbMCmButXNuLXQ6wWQCc0EAPQhW9tYPol3zPuhuA8aOs7oN14PrcTkI5JcrPnxaq/E7FOuHXugAAABAzhiEAwAAADljEA4AAADkjEE4AAAAkDMG4QAAAEDO1rRt/VZTCdr61oKWt512PDtBu+NnMogy4cUBZ2GBVyz4V7Ra89PVzC/45bLa1reDelQLZw1p+plV6nV/HkiSySf8i0X/Fi0Wfeq/GqT+JanV9tscVKcTvx7R8Tebfj/1BX8el4rxcW7XWSmy5oSoL/hz6ZFHv+Fqb3+rnzVpK86OEp0f4+N+uf37b13VNnG2lPwZ2m7560+0nBTPehLNztQddEqxjDdM6vr1Z+f8e2h+3tfqGTO7VKucH1h7jPYAAACAnDEIBwAAAHLGIBwAAADIGYNwAAAAIGcEM1cgBeGRffsuc7UDjx8O1zeLWqr78Mdll73B1UZGCIkMwgo+9FOwwWqpE4eDFuZ9ELEbBIGKRf/zLYS1cDdKnRFXK43sd7U9e17taseOHQm3GbWtbzabA9W63bhtfbvtW1F32n7ZlPw3mrHJ7SsjaNZs1IOqD5AtBG3rpd2rOqTNYrMEK5vNOPg3iPX/Hv19ukKx4mrdbnwid4MQZhTMbAVt59sdf7Eoj/hrZI/ffyN4Dz355GOuNj29J9wiwUzkgTvhAAAAQM4YhAMAAAA5YxAOAAAA5IxBOAAAAJAzgpkrEHXlevrbPugRhfay+R9BuxMECQv8qC5UGvBPzawOkbOzp11tLqhZwYeLRms+xNRYiJOZIzXfAnBiPOoUeJGrzZw+Gm5zdnbW1Z47/F2/nwkfToqCyL26Pz+jgGGp6ENUVuDv/qWijoJZ9axlsXFEIczZ2ZOuduTIo662b9/1rhZ1BZXyC2zWqqPBvn3X2zmLr2mdjr8mRp2nW0Fge7Q64Zdr+lC4JNVqZb9s21/75udPuVocbpamp3eFdeRvNeFmaSMEnLPxGxEAAADIGYNwAAAAIGcMwgEAAICcMQgHAAAAckbabwUaDR8KGbQmSWWfHVG77cOAJ0/6IM9Fu16Tsc1go9tYEBkMg4QpWDKrQ2SjMe9qB59+2NWuvvY6VysGQcRCIQ7Y1YLA0lVX3+gXXEE+b2Eh6rLov596UAsavEqSkvw5G3WDjcLE2zlamIKugt2U9Yr48yYF52cUnu0EnQYlqVjMaNWKFcsKikUhzANP3DvQNqOw5v79t67swIYsClFeddXrXe0bX38xXL/b9cHMoImvXve6N7naseN+m6+56opwP9997nFXmwtC6c2Gry3Mnwm3GQU2a7WxcFkMz2rCzdLgAeeNEtbkTjgAAACQMwbhAAAAQM4YhAMAAAA5YxAOAAAA5IxBOAAAAJAzZkdZgZERPxPJ+MS0q3W7M/EGrOlKzaavtYJaM6Nd7+iobyu8naVuMFtE0BO5G8wqEaX2JSl1/M/jssuvdbVwngsLNhq1fZdULgctoiu+Fs5aEu1HUieYnWBm5rirtTt+xpOsWVzKZf+3+0KwaNSyOuMl3hYsaO3dblfCZYslf2kujfjl5ub8zA5Zs/zkNTtKN3gPNur1gdYtZBxjpRK/ThtN1owNg4hmdVhv1WB2lErQyn583P8elKTZOT8byWjVzzBy+vQxV3vrTT/glwuuXZIkC94cwdWmFcy88cSB+8NN7tq1x9WYHWW4Bp0JZdAZhqSNOcvQuXAnHAAAAMgZg3AAAAAgZwzCAQAAgJwxCAcAAAByRjBzBao1H0jZvediV2s2j4brNxo+4Ndt+/DayZM+pNJq+XXhdYN0Zb3hX+NOsFzU3l6SqlXf3rYaBCartcGCb1kB0GbLhyOjY4paxGdFHqOQ3kLQurnR8MG5kSjrJKla9QHlmYLffzdFbdbj0OB2UK7482hqale47MLCS66Wgp/xwsK8q83O+nbbkjQWZMrKZf+zXK163e//xRefd7UTx/117uprfEt0afMEM6NwZRQUi1tr+3DjRmmtvVQtCGbWgt+NktTt+pMuBRfAq4K29dH3PjkRv186XX/tHQmu25J/vxSK8TWp2RwsTIzhWk24WdqYAedz4U44AAAAkDMG4QAAAEDOGIQDAAAAOWMQDgAAAOSMYOYKROGgXbt2u9pLL8WJtoV5H0iJugrWF04NtBy8TifomBl08Gs2oxBk3CGyHYRnU0aHSrdc0EczOsaseqfj9xN1sszIlIZf6LT9fp47csTVXv1q3zFOyui8GLwe7a4PQW3nnpmViu8+eNlrrwyXnZvzQcbZWX9deP67h12tvhCfX/tf70OPqwlm1jO6YJ4+7TvePf7Yfa42Nj7uas3m5Rl7izsyrpeswGTwLQ3crW8jhjAjlaCL5lVX+w7CkvT44y+7Wifoztto+I7QUSi9EoSbJemGN97iao88do/fT8lfu4qF+P0SdXNcWPChY7poDtdqws3S5gk4L+JOOAAAAJCzNRmEm9mPm1nq//f3M5a52cw+Z2YnzGzezB42sw+Z2WDzvAEAAACb1NAH4Wa2T9KvSfKTEb+yzA9LukfSbZL+UNKvSypL+riku4Z9TAAAAMBGMtRBuPW6iPy2pOOSfiNjmUlJn5LUkXR7SunvpZT+iaTvkfTnku4wszuHeVwAAADARjLsYObPSHqXpNv7HyN3SNot6XdTSvcvFlNKdTP7qKQ/k/RBbcA74qWSf7mi7oVzc3F3y6xA3nIj5SggONi6210UUGw1g8BjEE48MxcHzYp7d7paOwgXtdtBCLMddY2MA6AL9WhZv1xUy+rCGZmbO+1qL77wXVfbsSPugtdu+aBqMwhWjVajg9++5/FI0IJ0bGwyXPbVr97vat/+9jdc7YUXnnG1Vis+j6+4Mgo9XnjgsVH3wTVJevSRB1yt3phxtdFR/3rYgIHnjWojB8CGpRoEM8sZHU2LRf87c37enwtPf/thV5ue3hvU/EQIktQNriuvvfSNrnboGf8earX8tUuSvvnNe13tLW/9/mBJfz3P6iCKs0Xvl9WEm7O2uZEN7U64me2X9CuSfjWl5GPJr1gcnH8++No96vWVvdnMNkefYgAAAGCFhnIn3MxKkn5P0mFJ//Q8i1/T//jU8i+klNpmdkjSdZKukHTgPPv1t1x64vmSAAAAgA1gWI+j/DNJb5L0V1JK8b9RvmKq/9H/m/jZ9R3DODAAAABgo1n1INzM3qre3e//NaX056s/pL98wOq8DwemlN6ccUwPSLpxCMcCAAAADN2qnglf8hjKU5J+fsDVFu90T2V8fXLZcgAAAMCWsto74eOSru7/fz2aKUTSp8zsU+oFNj8k6UlJN/XXO+uZ7v6g/nJJbUk+9r/O6sFsAHNzvo1tVJOkovkE98iI700UtSWPaggMOJtIO5ipppDRJipqn9xs+hkoZk4dd7VGw58zWS3mC0EWOXUH+7lnLZWCc67d9rMBzM/7VuMvvuhb2Utx6+Z63c+Y0qj6fafMI92espL8k1NR62XfYr4czEBTGvGz7EhSt+tn9BlUo+HP91NBe3pJmpk55Wun/YwYU5MTrpY1s0vUQnyzzYKwlWXN/NXp+PNzYWHe1cbG/TmbdS5EKsG5EJ0fpbKf2WVuzp+vkiTz75eHHrrP1d7+9r/qasyOcuG22/t6tYPwhqT/kPG1G9V7Tvwr6g28Fx9V+YKk/1bSuyX938vWuU3SqKR7UkrxvEEAAADAJreqQXg/hJnVlv5j6g3C/4+U0m8t+dLdkv6VpDvN7NcW5wo3s6qkX+ov88nVHBcAAACwkQ27Wc95pZRmzOwD6g3Gv2Rmd0k6Iel96k1feLekT+d9XAAAAEBehtq2flAppc9Keqd6zXl+VNI/lNSS9BFJd6boIVwAAABgi1izO+EppY9J+tg5vv5VSe9Zq/2vhWbTP6b+xBOPB8vFbetr1SCUFoRZi0WfEMwIvWI5839XFgo+0FYs+uWKGbm10eBP1caCDw0dfemFYDl/ztRqY+F+ojBPVOt0g7BnxoyeUdi0WPYhqkbjmKudOBEHo6JTsdP1+28nvyB/Xp8tCslm1bvyJ2i57Fu/K/mQrCTNzZ5wtaNH/XJRmC4KZj726Lfi/QRBt7ExHzqu133A99lDvn25JI2O+rYR2yHAFQVSVyKv1yil+H7e/Lw/ZwvBRalQCK7HwTU6S7R+dJ01C363ZiTyOx3/HiyYfx80Gj5oCgxqXe6EAwAAANsZg3AAAAAgZwzCAQAAgJwxCAcAAABylvsUhVvNRbv2uNrsrO+cKEmVsg9sdrpBAKvru4d1O3EXvHbbB19Kpe37Yy0Wo6Cr/1uzPOIDbZYyOg0GQbV20DGz1fI/33rD/3x3TO0M93P1Nde7WrXmg1UzMz7Q1m5ndKwLyqOjPiQ3N+uPMwomSXFIOCV/zkWBp6wQ1HYQhexmZ+Oukwefud/VoiBj1Em3mxHM/Pa3vxEsO9jPY/aMD59F3WAlqeqbEmZsc9bVCoX49Yi6FU9ODrafzWIl58eRI4+62r59/voxPu7XXYuwZqUSb3N8zF/rOu0zrlYIOummIOzd7cbXuSiYGYUw201/cqZ4kyoE17lW21/jWy1/nazX41B7ddA3xyaxWYLDGxl3wgEAAICcMQgHAAAAcsYgHAAAAMgZg3AAAAAgZ9s3wXcBqlXfvfDa/W90tZ27fHc3SXr4kXt8MUiFRKG/Z7/zdLjNqBvk+MSUq0WdF7eiIJ+j8TFfbDZ8Lcm/lpJkQRfOThAQajZ80KzT8iG5VjvociipXPaByWrVB1dOn/aBodnZuEtr9Hd2MQjzlYr+e8/ubhmFX/33VB3x51wx+gFtY1HALks3CKo160F4reQDnJLULvpwZXQet9t+mykIJ9eqGb8+zC87N+fDa/PzfrnRoLOmJGXk8TatQUOYB564d+BtRufSlVe+ZWUHtsygwbmsLsBR2PyRh/3EBQtBB+LTM77z6ujYdLif0VG//0pw7CMlX+t24+uxmQ/qp46/nj/8rQdc7cY3x79vd+zwQdXoGr8RDXrODhoalvILDm9k/EYEAAAAcsYgHAAAAMgZg3AAAAAgZwzCAQAAgJwxCAcAAAByxuwoK1Au+xkkotrCgm/LK0mVik9M1+d9Krxe97MYPPXkg+E2jx075mrveMf3udr2mR0lmrnDL1cq+eU6HV+TJAtmDgk6Gqvbbbtasx3Umv7n29t/5nQkZ0kpmN2kFMTMJWn+hCtFbeejGWAsmAWlV/dKJf8aVYIZC6Ll1sJmaaecNWtANMPAnt1XudqjjwSt6Lvx7ChmfoqRkZL/uReiTvbBuVksxvdwiiPR+8Vff8bGfG1yale4zWo1nn1jK1nJTDmRvRdf6Wqzc/79/9LBg+H6q2l7n9WOvRy0sy8WJ1xtfv6oqz377FOutnPnxeF+wtlRgmO66pr9rjZz/8vhNrsdPztK9Lu52fQz/zz04F+E23z7zbe72kabHSXr2rma2Xuyzu39+28d/MC2KO6EAwAAADljEA4AAADkjEE4AAAAkDMG4QAAAEDOCGbmqFTwQSQzH5zpdn1r3Ln50+E2q9VJV6vXo2DWRec/wK0gSg3G+cJA/Ddpu+0DOt2gp3sUaKsv+HUndsThxFJpsLdjuewDR+WRuN13MQiVtlo+DNzt+u89pbhXeBTsLJV86+fx8Sl/PAN+jysxaDtlafCWymvRTjlaP9qPFAeW5uZ84Hv/fn/Sfetb94Xb7LRnXa1QHCwMHP3MC8G5JUljoztc7YYb3upqpZK/HlarcYA8K/i3lUTnYVagLQphRg4evH/g/Uf7Wm1wLgqRR7VGEG4c6/rzNbp2ZalU/DVxdNS/4fbseVW4/nPfPe5q0TWx2WwGtTjcWF8Ijt+/XTak1QSHswLo4E44AAAAkDsG4QAAAEDOGIQDAAAAOWMQDgAAAOSMYOYaKBQyXlbz3RNNPlhlQcBv57TvMiZJ5UrQ8S7oGrltDJYzC1nG36Tdtt/oieO+U2kU2ikFgcldO+Ig0KDdJKMQ5J698TbPzD7naieOB6Gh4JxJFh9P9BJXgs6xcS0OkA5q0BDmoJ3cpLUJpA1qJWHPUil67fzFYvfzl4brHzl8wNXaHd8BMApBRqHh8Yk4UfaGN36vq01O+k6YeXUl3YgGDelmnYfNpg/4HTzou6euxFqE56pBx8yoQ+Roy59zheByHNVWdDzBvseCALkkFYIuryr5q9/0tP/BjY3FietqbfOe84MGh+Og+3S4ze18DVjEnXAAAAAgZwzCAQAAgJwxCAcAAAByxiAcAAAAyBnBzDWQ1d1tfGy3qzUbp1wtCviVR3wYT5KqQTCz1fLdx+p1H2iLQip5WljwnT2jWqRWGwvrKehk2fVNK8OOl1mtNRsNH4KKXuN2UBsb9YHaWsbrXsvoFrhcdH6NjMTnnAVv8XIl6FjX8F3fUgoSwpIKRX8uFkt+2VddvNfVSiPDv+SsppObtHm6uRWCVFrUAfDKK68J1z9+7AVXO3nSvzlmZnzH3osv9sHf17/+lnA/4xM7XY0A1vmt9jVaTXBOisNzqz2mSnCtuvqa/a722GMvu1qp6K8pWV18u11fj94vI0Gofd++uPtotM3Dh/3rOT7qr9tvuOEt4TY3Q+fXrJ/5SoLDg24T3AkHAAAAcscgHAAAAMgZg3AAAAAgZwzCAQAAgJwRzFwDlUocsLvstVe42pkzh11tYcGHKEeCjnWS1G77MOAjD3/d1V5/nQ9gTU35LnaSVA0CgoWojeeAonCOFIcw/+IbXxpom297y+1hvdv1gcsodNht+9BNShkdM7s+qDY3/6KrmYKOmUEQaG8QcpOkUkb4drliEFiampoMlzXz2xwp+e+z2w1CTMX49Yi61k1O+ODNSPD9RMGo1Ro0kJa17FoE0vJSqfgumjt2xN3pLr74NcH6Pm3V7vj3y949vgvn1NRF4X6qGdc/rK3onL3ySh8QLJfj6/FanPNREDH6fRCF1Vttf92dn/cdXiVpenqwYGZ0ja1U4tdjcsK/j0aKvkvsla97o193cut1iNzMx76RcSccAAAAyNlQBuFm9qyZpYz//C3D3jo3m9nnzOyEmc2b2cNm9iGzVdxyBQAAADaBYT6OclrSvw3qs8sLZvbDkj4jqS7p05JOSHqvpI9LukXS+4d4XAAAAMCGMsxB+KmU0sfOt5CZTUr6lKSOpNtTSvf36z8v6QuS7jCzO1NKZf3tiwAAGJNJREFUdw3x2AAAAIANYz2eCb9D0m5Jdy0OwCUppVSX9NH+px9ch+MCAAAAcjHMO+EVM/sxSZdKmpP0sKR7UkrLp+V4V//j54Nt3CNpXtLNZlZJKfmpPzaBaMYCSRoplV2t2fSzeXQ6wSwbxajNutRs+plUusnPbvDAA3/qapOTPuktSddd9zZXix7VP/ydZ13t0ste62pZ6fMTJ4+62skTz4fL+nXj5YpBIr7Z8m3nZb51e7EYt61P5lP6Fqxv8j8jMz8rTavtntCSJJ0541s3D6rROBPWK1V/LjWb/jwcKftLQdaMPBb86d4IzsOF+ilXO3Fi+BMyNVt+33svjltRR8vOhj+Ok6s8qvXTaPjvUZJec+k+V9u127eYt+AHHM1yMTd/PNxPO+P8xvBE5/FLLx50teh9UG5mzXKRzzk/O+v3E30/7XZ7oHUl6cSJ77paNDNVpNmMhxnPPPOYq3WT/13Q7vj1s45zM19X8hLNwhLN6LNVZmsZ5m/EiyX93rLaITP7uymlLy+pXdP/+NTyDaSU2mZ2SNJ1kq6QdOBcOzSzBzK+dO1ghwwAAADkb1iPo/y2pO9TbyA+JukNkv43Sa+V9F/M7IYly071P57O2NZiPb5NCwAAAGxyQ7kTnlL6hWWlRyX9AzOblfSPJX1M0t8YcHOL/84fP39x9n7fHG6gd4f8xgH3BwAAAORqrYOZv9H/eNuS2uKd7inFJpctBwAAAGwpa922fjF5N7ak9qSkmyRdLemsZ7rNrCTpckltSc+s8bHlrlTygc1S0QcOmsGPJWX8w0DYWTwIj3S7PqwZdHOXJD362L1+k0Fb86hF/KnTz7ra6GgcoKg3fAvisTG/nyiA8fLLh8JtpuCYykHocGEheD2TDzFKUrXqQ5iFQhTi9LWJCd/C++jR+NhbrThcuVyz6X9wZ86cyFjaB0PLFR+y7b31BhOdibWaD3sefcm/hesL/G29kQT54lC97oNz9Tohs2GKQvbNZhAqlzQ759/v0XWyediHC8fH45bqeZmZ8YHebvJXlXoQMH7++ccztvmCq42Ojrta9Bo1GnEwc2zcXxOj2qnTfpKARoP3xoUaCX5Gl+67bh2OJB9rfSf8Hf2PS38bf6H/8d3B8rdJGpV032adGQUAAAA4n1UPws3sOjNz81yZ2WWS/l3/099f8qW7JR2TdKeZ3bRk+aqkX+p/+snVHhcAAACwUQ3jcZT3S/ofzeyLkg5JOiPpSkk/KKkq6XOS/pfFhVNKM2b2AfUG418ys7vUa1v/PvWmL7xbvVb2AAAAwJY0jEH4F9UbPL9JvcdPxiSdkvQV9eYN/72Uzn7gK6X0WTN7p6Sfk/Sj6g3Wn5b0EUmfWL48AAAAsJWsehDeb8Tz5fMu6Nf7qqT3rHb/m0mtNuZql13uAwdPP/0XrtbuxF3oRko+ZFcq+JpGfAIrBQFOSVqY9+G5udmo86IPCJYrfrn5eR/ak6Ri0R9nq+WPKeq4efHeuCNiFBZ9/nnfTa3V9N3YFubjYGaUXpvc4b+nYtBVtO6zp3rd664Kd7Njh+9eGGm2/Ot++nTcJbHV8sHM6DUqRQnfjIfVuh2/fvRX897gZzQ5uSveKLDNRV0jDx68f1Xb3Hep//1SHlnfToPVqp8Y7eWX/9zVWkEodX4h/j0Yda3cveetrhaFUtf79cDZtkonzEGtdTATAAAAwDIMwgEAAICcMQgHAAAAcsYgHAAAAMjZWnfMxBK1UR/MvOSSfa727OEnXK29EPcuagRBxpERHxAspgFb40lSkE8sl4MwX8mfPu12RhvOSDQJThAabNb9sddqPtzT45ctlydcbW7Wh0+bTR9ilKRuEDsc6/jvvTTiw5qViu/aNjGxO9zPzp17w/pyUSD2ta+NQ7Zf/4vDQdX/gEtBwDejSas6QTCzvuDXr9V2uNrOnZfEGwW2uahjZvnaW8Nljxx51NX27bverx+E3MplH3TPshYhuWYQii8WfTfp6DpTrcYh/+g452Z9V9Go8+J2CwJi5cL35pDOG+6EAwAAADljEA4AAADkjEE4AAAAkDMG4QAAAEDOGIQDAAAAOWN2lBwVCv5vnlrNz55x0423udqDD3413Ga9fszVSoWorbhPpGfNfhEcpiplP/tFt+tn2YhazGfzM5l0gtlRwlqQnJfimWHGx/xpPjvja90UzzCSLGpnH80WM+Jquy7yM55Ey61EuexnCCiV/OwCkjQy4uvN5pxfMHo5MybUKY/4n3E048IIsw4AA4tmWxj3vx4kSfv3+1lTmkGb92hWh4MHvxFuM5pdJdr/6meF8L9g2m1fKxT9tbxa8TOMSVK1Oupq0fcDnEv0fpGk2dmTrvbV+37jrM9PnTp6QfvkTjgAAACQMwbhAAAAQM4YhAMAAAA5YxAOAAAA5Ixg5jqr1XygxMwn4m666Z3h+t951re4f/75x12t3fYt2aOgqCQVo3IQ0staf1BRsLPTCkKlJb9zy0gNRodUDIqFoEu7BYHW3r6ibfpqreYDS9XqYLXVympFPTq2M6gGwUz586Ncio+zNurb0e9//S1+uSAsBWBwWSHIKEAW1Q48ce/A+zpy5FFXiwKgq+evxyPRtSb568f4+HS4xf3X+uOMlqVFPS5E9N4YFu6EAwAAADljEA4AAADkjEE4AAAAkDMG4QAAAEDOCGZuQFFwrxh0D5OkEyd8+GThkF9ubtZ3zBwdjcONI0FDx1K0/2D1btuHG4P8pSSpE3xhYcEfZ7UanKZBeDXroFIUKg3WLxbjbVYqwfee/PdZCNKeuy7a42ojI8N/25XLccfMi/e+2tVeOnrG1QpBV9CJ8SjUKV151U2uRggKWF+rDY/l1WEyCmxPTvprjZkPm19z7ZvDbXL9wVqK3huHjzw2lG1zJxwAAADIGYNwAAAAIGcMwgEAAICcMQgHAAAAckYwc5OIQn+StGvXXlebmtrtF0z+R53CzolS1DeyNjbpapUg+NKot3yt4bsxSlK367+nSsUHM8fHfOimVh0Lt9np1l0t7HgZdOGcmCiH24z59esNf+xRV9FSKUi+rlK1EoeQJib9a9dqvcrVLrnkClcbn4iDmVF3TkJQwPqKwmNRWDMrgJlXuLFS9deP699w40DrVoN1Ja4/GI6s82h83Nfe+0MfPuvzf/mLfyTphRXvkzvhAAAAQM4YhAMAAAA5YxAOAAAA5IxBOAAAAJAzBuEAAABAzpgdZZPIals/Pu5nLXn7297laqdPH3e1gwcfCrcZTOihq65+k6tVK779cKfj259nta3vBl+oL/jZTaan/SwdtVqcYp6b9+tH06MUi/6brFUHnx2l1fQzvnQ7fl6ZqLYWiqX4/Nizx8+EEtVKwfrMOABsTNF7M5rBYf/+W1e1zbUQzXCSNesJsBGs5XuDO+EAAABAzhiEAwAAADljEA4AAADkjEE4AAAAkDOCmZtcrebbt0e10VEfopye3jXwfqpVv34lCGauRBTMHFTUDl6S5uZPDrS+BasXFIcbo7b3bfPH3vFZTeUTy8wO7o6OBmktAFsOQWpsVM3mwqrW38rn9lDvhJvZrWb2GTN7wcwa/Y9/YmbvCZa92cw+Z2YnzGzezB42sw+ZWTyaAAAAALaIod0JN7OPSvoXko5J+k+SXpB0kaQ3Sbpd0ueWLPvDkj4jqS7p05JOSHqvpI9LukXS+4d1XAAAAMBGM5RBuJm9X70B+J9K+pGU0pllXx9Z8v+Tkj4lqSPp9pTS/f36z0v6gqQ7zOzOlNJdwzg2AAAAYKNZ9eMoZlaQ9K8kzUv628sH4JKUUmot+fQOSbsl3bU4AO8vU5f00f6nH1ztcQEAAAAb1TDuhN8s6XJJd0s6aWY/KOl69R41+XpK6c+XLb/YzvHzwbbuUW8wf7OZVVJKjSEcHxSHKFcbrFytrHDlaqTko5ApSFYGjT1VyjicTtD1csk/7iyp+bdTqeSXA7Ayqwl2beVQF7DRRO/V2dl4woQjRx51tX37rne1qBvsVnlfD2MQ/pb+x5ckPSjpDUu/aGb3SLojpfRyv3RN/+NTyzeUUmqb2SFJ10m6QtKBc+3YzB7I+NK1gx06AAAAkL9h3Ir8/9u7+2C56ruO4+9vSklDm4cGiXQqBlqDjdOZDoK0BAmgLX2Q1jLFlnFEdIQRR4uUOmXGh8of1T4oAtNWsWJhLCg20ba2RUENIZRoaa2tSnk0XG0tECENCSUJT1//+P1W171nc+8me8/u3ft+zZw52fOw99zzye797tnf+f1W1fmFwBLgtcBSytXwm4H1wIau7ZfX+eN9nq+zfMUQjk2SJEkaO8O4Et7pUjAoV7y/Vh/fFRFnUa54nxoRJzU0TWnSaTwwYxfLmXl84xOUK+Q/OIufJUmSJLVuGFfCO419tnUV4ABk5h7K1XCAE+u8c6V7Oc2W9WwnSZIkTZRhXAm/t8539lnfKdI7rejvBU4AjgX+X5vuKHe2HQM8A2wbwrFpgTl08fSbNZYsXjZt2cqV0/c9ZFHzOFHPNdzFuWv309OWfe/qtdN/dsPopZKa9bsBs+nGroO5qQsm58YuaVRmexPm3ffcPuvnbHpdr117ymAHNo8M40r4FkrRvCYiDm1Y33lXnKrzTXX+hoZt1wOHAVvtGUWSJEmT6qCL8Mx8lDLq5XLgvd3rIuJ1wOspTUs6XRJupIyqeU5EnNC17QuA99WHf3CwxyVJkiSNq2ENW38J8Grg1yJiPXAnsBo4izIy5gWZuRMgM3dFxAWUYnxzRNxIGbb+LZTuCzdSinpJkiRpIg1ltJTM3E4pwq8AjgIuogzK83nglMzc0LP9p4FTKU1Z3ga8E3iaUsyfk00jrkiSJEkTYlhXwsnMHZQi+pJZbn8H8KZh/XxJkiRpvhhaES6Ng8UNPR6s/YGTpi17at/eacsWLWoY3x54tqF3lEUNQ9Q39syy5LDG55QWukGGt55t7woLrWcFadw0vQYH0dTD0SQbSnMUSZIkSbNnES5JkiS1zCJckiRJaplFuCRJktQyb8zURFm8ePqNkE3LWNrCwUjqq2nY+Du2Xn1Qz3nyuncc1P6SZq/pNfyqV50xbdlnP3dF4/5vPvNdQz+m+cYr4ZIkSVLLLMIlSZKkllmES5IkSS2LSRwhPiIeW7JkycpXrD121IciSZqlnTu3H9T+K1asGtKRSBqWfq/rSXq93nP3fezZs2dHZh4+yH6TWoQ/CCwDpoBX1MX3jOyAtD/mM/7MaLyZz/gzo/FnRuNt3PM5GtiVmccMstNEFuHdIuKfADLz+FEfi6Yzn/FnRuPNfMafGY0/Mxpvk5qPbcIlSZKkllmES5IkSS2zCJckSZJaZhEuSZIktcwiXJIkSWrZxPeOIkmSJI0br4RLkiRJLbMIlyRJklpmES5JkiS1zCJckiRJaplFuCRJktQyi3BJkiSpZRbhkiRJUssmtgiPiO+JiI9HxLciYl9ETEXElRHx4lEf20IQEYdHxPkR8amIeCAi9kTE4xHxhYj4uYho/L8XEesi4qaI2BERT0bEv0TExRHxvLZ/h4UoIs6NiKzT+X22MaOWRcQpEfEXEfFQfT97KCJuiYg3NWxrPi2LiB+reXyzvtdti4gNEXFSn+3NaMgi4uyI+HBE3B4Ru+p72PUz7DNwDhFxXkTcGRFP1L9pmyPizOH/RpNlkHwiYk1EXBoRmyLiGxHxVEQ8EhGfiYjTZ/g58yufzJy4CXg58AiQwKeBDwCb6uN7gMNHfYyTPgEX1vP9LeAG4P3Ax4GddflG6mBRXfv8OPAM8ATwx8Dv1LwS2DDq32nSJ+Coms/ues7Pb9jGjNrP5dfr+f1v4Frgt4GPAV8CPmQ+I8/ng/X8PgpcU//ebASeAp4DfsqMWsnhq/Uc7gburv++fj/bD5wD8Lt1/TeAK4CPAo/VZb806nMwztMg+QA31vV3AX9Y64e/rHklcNGk5DPyA5ijsG+uJ/2dPct/ry6/etTHOOkT8CPAm4FFPcuPBP6z5vC2ruXLgO3APuCEruUvALbW7c8Z9e81qRMQwN8B/17/GE0rws1oJLn8RD2vfwssbVj/fPMZaT5HAs8CDwOretadXs/5NjNqJYvTgTX1vey0GYq8gXMA1tXlDwAv7lp+dC309gJHj/o8jOs0YD4/AxzXsPxUyofbfcBLJiGfiWuOEhEvA84Apiifgrr9JvAd4NyIeGHLh7agZOamzPxsZj7Xs/xh4Or68LSuVWcDRwA3ZuaXu7bfS7kSCPALc3fEC95FlA9OP0t5jTQxoxbVJlsfBJ4EfjIzd/duk5lPdz00n/atpjTr/GJmbu9ekZm3Uq76HdG12IzmSGbempn3Z628ZnAgOVxY57+Vmd/u2meKUmssprx/qsEg+WTmdZn5zw3LbwM2A4dSiu5u8zKfiSvCKYUEwC0NBeBu4A7gMOA1bR+Y/lencHima1knt79p2H4LpRBZFxGL5/LAFqKIWEv5Cv2qzNyyn03NqF3rgGOAm4Bv13bHl0bEL/dpa2w+7bufcmXuxIj4ru4VEbEeWEr5hqnDjMbDgeSwv33+umcbzZ2m+gHmaT6TWIR/f53f12f9/XV+bAvHoh4RcQjw0/Vh94ulb26Z+QzwIHAI8LI5PcAFpubxCUoToV+dYXMzatcP1fkjwFeAz1E+LF0JbI2I2yKi+yqr+bQsM3cAlwLfDXw9Ij4WEe+PiE8Ct1CaEf181y5mNB4GyqF+c/5S4InMfKjh+awrWhARq4EfpXxI2tK1fN7mc8ioD2AOLK/zx/us7yxf0cKxaLoPAK8EbsrMm7uWm9tovBc4DvjhzNwzw7Zm1K5VdX4hpSh4LfBFShOIy4HXAxv4v2Zd5jMCmXllRExRbjy/oGvVA8B1Pc1UzGg8DJqDuY1Y/VbiBkqzkvd0NzlhHucziVfCZxJ1Ppt2YxqiiLgIeDflDvRzB929zs1tSCLiRMrV78sz8x+G8ZR1bkbD0ekmLYCzM/PvM/OJzLwLOAv4JnBqv27wGpjPHIiI91B6Q7mO0jPXC4HjgW3ADRHxoUGers7NaLQONAdzmwO1y8hPACcDf07pBeVAjF0+k1iEdz7xLO+zflnPdmpBRPwicBXwdeD0+jVuN3NrUVczlPuA35jlbmbUrs6Vnm2Z+bXuFfVbi843SSfWufm0LCJOo9w8+1eZeUlmbsvMJzPzK5QPSv8FvLt2GABmNC4GzWGm7We6EqsDVAvw6yk9RX2S0uVnbzE9b/OZxCL83jrv1/ZnTZ33azOuIYuIi4GPAP9GKcAfbtisb261YDyGciPGtrk6zgXmRZRzvRbY2zVAT1J6EQL4o7rsyvrYjNrVOd87+6zvFOlLerY3n/Z0BgG5tXdFZj4J3En5O3tcXWxG42GgHDLzO5QPVC+KiJc0PJ91xRyoWfwZcA7wp5ReonpvyJzX+UxiEd55MzwjekZljIillK8z9gD/2PaBLUQRcSml0/yvUgrw7X023VTnb2hYt57So83WzNw3/KNckPZRBqhomjpdQ32hPu40VTGjdm2hFAJrIuLQhvWvrPOpOjef9nV6zziiz/rO8qfq3IzGw4HksL993tizjQ5Sfc/bSLkC/ifAuZn57H52mZ/5jLqj8rmYcLCesZgozRwS+DKwcoZtl1FGBHQQi9Hndhn9B+sxo3azuL6e1/f1LH8dZTTGncAK8xlZPm+v5/Vh4KU9695YM9pDHaXZjFrL5TRmHqxnoByYp4PBjOM0i3wWA5+v21xDz6B/ffaZl/lEPciJEhEvp7yQVgGfoQyR+mrKiE33Aesy87HRHeHki4jzKDcqPQt8mOa2WFOZeV3XPm+lfPLdSxm2dgfwFkp3UhuBt+ck/ocdMxFxGaVJygWZeU3POjNqUUSsooxt8H3A7ZTmDasp7Y2T8vXshq7tzadF9dvWmyk91+wGPkUpyNdSmqoEcHFmXtW1jxnNgXpe31ofHknpPWgb5XUD8Ghm/krP9gPlEBGXA5dQboreSBk05h3A4ZSLfh+Zk19uAgyST0RcSxk181Hg92m+oXJzZm7u+RnzL59RfwqYw09aRwHXAg9Rvgr8D8qNgfu9Ius0tPN/GeWFs79pc8N+J1MHJ6FcQfpX4F3A80b9Oy2UiT5Xws1oZHmspHyL92B9L3uMcnHhNeYz+gl4PnAxpYnjLkoTou2Uft3PMKPWcpjpb87UMHIAzgO+RBlZeDdwG3DmqH//cZ8GyYcyKuZM9cNlk5DPRF4JlyRJksbZJN6YKUmSJI01i3BJkiSpZRbhkiRJUssswiVJkqSWWYRLkiRJLbMIlyRJklpmES5JkiS1zCJckiRJaplFuCRJktQyi3BJkiSpZRbhkiRJUssswiVJkqSWWYRLkiRJLbMIlyRJklpmES5JkiS1zCJckiRJaplFuCRJktSy/wFl0e74a2UnkAAAAABJRU5ErkJggg==\n",
      "text/plain": [
       "<Figure size 432x288 with 1 Axes>"
      ]
     },
     "metadata": {
      "image/png": {
       "height": 213,
       "width": 368
      },
      "needs_background": "light"
     },
     "output_type": "display_data"
    }
   ],
   "source": [
    "data = CaptchaSequence(characters, batch_size=1, steps=1)\n",
    "[X_test, y_test, _, _], _  = data[0]\n",
    "plt.imshow(X_test[0])\n",
    "plt.title(''.join([characters[x] for x in y_test[0]]))\n",
    "print(input_length, label_length)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 准确率回调函数"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-06-16T06:51:41.920047Z",
     "start_time": "2019-06-16T06:51:41.911070Z"
    }
   },
   "outputs": [],
   "source": [
    "from tqdm import tqdm\n",
    "\n",
    "def evaluate(model, batch_size=128, steps=20):\n",
    "    batch_acc = 0\n",
    "    valid_data = CaptchaSequence(characters, batch_size, steps)\n",
    "    for [X_test, y_test, _, _], _ in valid_data:\n",
    "        y_pred = base_model.predict(X_test)\n",
    "        shape = y_pred.shape\n",
    "        out = K.get_value(K.ctc_decode(y_pred, input_length=np.ones(shape[0])*shape[1])[0][0])[:, :4]\n",
    "        if out.shape[1] == 4:\n",
    "            batch_acc += (y_test == out).all(axis=1).mean()\n",
    "    return batch_acc / steps"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-06-16T06:51:41.927035Z",
     "start_time": "2019-06-16T06:51:41.921727Z"
    }
   },
   "outputs": [],
   "source": [
    "from tensorflow.keras.callbacks import Callback\n",
    "\n",
    "class Evaluate(Callback):\n",
    "    def __init__(self):\n",
    "        self.accs = []\n",
    "    \n",
    "    def on_epoch_end(self, epoch, logs=None):\n",
    "        logs = logs or {}\n",
    "        acc = evaluate(base_model)\n",
    "        logs['val_acc'] = acc\n",
    "        self.accs.append(acc)\n",
    "        print(f'\\nacc: {acc*100:.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 训练模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-06-16T08:35:47.053701Z",
     "start_time": "2019-06-16T06:51:41.929060Z"
    },
    "scrolled": false
   },
   "outputs": [],
   "source": [
    "from tensorflow.keras.callbacks import EarlyStopping, CSVLogger, ModelCheckpoint\n",
    "from tensorflow.keras.optimizers import *\n",
    "\n",
    "train_data = CaptchaSequence(characters, batch_size=128, steps=1000)\n",
    "valid_data = CaptchaSequence(characters, batch_size=128, steps=100)\n",
    "callbacks = [EarlyStopping(patience=5), Evaluate(), \n",
    "             CSVLogger('ctc.csv'), ModelCheckpoint('ctc_best.h5', save_best_only=True)]\n",
    "\n",
    "model.compile(loss={'ctc': lambda y_true, y_pred: y_pred}, optimizer=Adam(1e-3, amsgrad=True))\n",
    "model.fit_generator(train_data, epochs=100, validation_data=valid_data, workers=4, use_multiprocessing=True,\n",
    "                    callbacks=callbacks)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 载入最好的模型继续训练一会"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-06-16T09:16:19.684510Z",
     "start_time": "2019-06-16T08:35:47.057236Z"
    }
   },
   "outputs": [],
   "source": [
    "model.load_weights('ctc_best.h5')\n",
    "\n",
    "callbacks = [EarlyStopping(patience=5), Evaluate(), \n",
    "             CSVLogger('ctc.csv', append=True), ModelCheckpoint('ctc_best.h5', save_best_only=True)]\n",
    "\n",
    "model.compile(loss={'ctc': lambda y_true, y_pred: y_pred}, optimizer=Adam(1e-4, amsgrad=True))\n",
    "model.fit_generator(train_data, epochs=100, validation_data=valid_data, workers=4, use_multiprocessing=True,\n",
    "                    callbacks=callbacks)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-06-16T09:16:19.729405Z",
     "start_time": "2019-06-16T09:16:19.686054Z"
    }
   },
   "outputs": [],
   "source": [
    "model.load_weights('ctc_best.h5')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 测试模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-06-16T09:16:20.917277Z",
     "start_time": "2019-06-16T09:16:19.731259Z"
    }
   },
   "outputs": [],
   "source": [
    "characters2 = characters + ' '\n",
    "[X_test, y_test, _, _], _  = data[0]\n",
    "y_pred = base_model.predict(X_test)\n",
    "out = K.get_value(K.ctc_decode(y_pred, input_length=np.ones(y_pred.shape[0])*y_pred.shape[1], )[0][0])[:, :4]\n",
    "out = ''.join([characters[x] for x in out[0]])\n",
    "y_true = ''.join([characters[x] for x in y_test[0]])\n",
    "\n",
    "plt.imshow(X_test[0])\n",
    "plt.title('pred:' + str(out) + '\\ntrue: ' + str(y_true))\n",
    "\n",
    "argmax = np.argmax(y_pred, axis=2)[0]\n",
    "list(zip(argmax, ''.join([characters2[x] for x in argmax])))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 计算模型总体准确率"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-06-16T09:16:44.017485Z",
     "start_time": "2019-06-16T09:16:20.918496Z"
    }
   },
   "outputs": [],
   "source": [
    "evaluate(base_model)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 保存模型"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-06-16T09:16:44.073896Z",
     "start_time": "2019-06-16T09:16:44.018652Z"
    }
   },
   "outputs": [],
   "source": [
    "base_model.save('ctc.h5', include_optimizer=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "# 可视化训练曲线\n",
    "\n",
    "```sh\n",
    "pip install pandas\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-06-16T09:17:29.985054Z",
     "start_time": "2019-06-16T09:17:29.852065Z"
    }
   },
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "\n",
    "df = pd.read_csv('ctc.csv')\n",
    "df[['loss', 'val_loss']].plot()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-06-16T09:17:31.278107Z",
     "start_time": "2019-06-16T09:17:31.050105Z"
    }
   },
   "outputs": [],
   "source": [
    "df[['loss', 'val_loss']].plot(logy=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2019-06-16T09:17:33.107543Z",
     "start_time": "2019-06-16T09:17:32.987147Z"
    }
   },
   "outputs": [],
   "source": [
    "df['val_acc'].plot()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
